import { useGraphQLRecordStore } from "features/apiClient/hooks/useGraphQLRecordStore";
import { RequestContentType, RQAPI } from "features/apiClient/types";
import GraphQLClientUrl from "./components/GraphQLClientUrl/GraphQLClientUrl";
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { toast } from "utils/Toast";
import { useDispatch, useSelector } from "react-redux";
import { useGraphQLIntrospection } from "features/apiClient/hooks/useGraphQLIntrospection";
import { useDebounce } from "hooks/useDebounce";
import { RBACButton, RevertViewModeChangesAlert, RoleBasedComponent } from "features/rbac";
import { Conditional } from "components/common/Conditional";
import { getUserAuthDetails } from "store/slices/global/user/selectors";
import { notification, Space } from "antd";
import { ApiClientBreadCrumb, BreadcrumbType } from "../components/ApiClientBreadCrumb/ApiClientBreadCrumb";
import { KEYBOARD_SHORTCUTS } from "../../../../../../../constants/keyboardShortcuts";
import { useGenericState } from "hooks/useGenericState";
import { BottomSheetLayout, useBottomSheetContext } from "componentsV2/BottomSheet";
import { BottomSheetPlacement, SheetLayout } from "componentsV2/BottomSheet/types";
import { GraphQLRequestTabs } from "./components/GraphQLRequestTabs/GraphQLRequestTabs";
import { ApiClientBottomSheet } from "../components/response/ApiClientBottomSheet/ApiClientBottomSheet";
import { ClientCodeButton } from "../components/ClientCodeButton/ClientCodeButton";
import "./gqClientView.scss";
import { GraphQLRecordProvider } from "features/apiClient/store/apiRecord/graphqlRecord/GraphQLRecordContextProvider";
import { isNull } from "lodash";
import { useLocation } from "react-router-dom";
import PATHS from "config/constants/sub/paths";
import { useAutogenerateStore } from "features/apiClient/hooks/useAutogenerateStore";
import { AutogeneratedFieldsNamespace, createHeaderEntry } from "features/apiClient/store/autogenerateStore";
import { useApiClientRepository } from "features/apiClient/contexts/meta";
import { useScopedVariables } from "features/apiClient/helpers/variableResolver/variable-resolver";
import { SendQueryButton } from "./components/SendQueryButton/SendQueryButton";
import { DEFAULT_REQUEST_NAME, INVALID_KEY_CHARACTERS } from "features/apiClient/constants";
import { Authorization } from "../components/request/components/AuthorizationView/types/AuthConfig";
import {
  trackAPIRequestSent,
  trackInstallExtensionDialogShown,
  trackRequestRenamed,
  trackRequestSaved,
  trackTestGenerationCompleted,
  trackTestGenerationFailed,
  trackTestGenerationStarted,
} from "modules/analytics/events/features/apiClient";
import { extractOperationNames } from "./utils";
import { GrGraphQl } from "@react-icons/all-files/gr/GrGraphQl";
import { useApiRecordState } from "features/apiClient/hooks/useApiRecordState.hook";
import { useNewApiClientContext } from "features/apiClient/hooks/useNewApiClientContext";
import ErrorBoundary from "features/apiClient/components/ErrorBoundary/ErrorBoundary";
import { getContentTypeFromResponseHeaders, getRequestTypeForAnalyticEvent } from "../../../utils";
import { useGraphQLRequestExecutor } from "features/apiClient/hooks/requestExecutors/useGraphQLRequestExecutor";
import { isExtensionInstalled } from "actions/ExtensionActions";
import { isDesktopMode } from "utils/AppUtils";
import { globalActions } from "store/slices/global/slice";
import {
  buildAutogeneratedTestsBlock,
  hasTests,
  injectAutogeneratedTests,
} from "features/apiClient/helpers/testGeneration/buildPostResponseTests";
import { useDeepLinkState } from "hooks";
import { RequestTab } from "../http/components/HttpRequestTabs/HttpRequestTabs";

interface Props {
  recordId: string;
  notifyApiRequestFinished: (entry: RQAPI.GraphQLApiEntry) => void;
  onSaveCallback: (apiEntryDetails: RQAPI.GraphQLApiRecord) => void;
  isCreateMode: boolean;
  openInModal?: boolean;
}

const createApiRecord = (entry: RQAPI.GraphQLApiEntry, record: RQAPI.GraphQLApiRecord) => {
  return {
    ...record,
    data: { ...entry },
  };
};

const GraphQLClientView: React.FC<Props> = ({
  recordId,
  notifyApiRequestFinished,
  onSaveCallback,
  isCreateMode,
  openInModal = false,
}) => {
  const [
    url,
    response,
    testResults,
    hasUnsavedChanges,
    introspectionData,
    isFetchingIntrospectionData,
    hasIntrospectionFailed,
    updateEntryRequest,
    updateEntry,
    getEntry,
    updateEntryResponse,
    setHasUnsavedChanges,
    updateEntryTestResults,
    updateEntryScripts,
    scripts,
  ] = useGraphQLRecordStore((state) => [
    state.entry.request.url,
    state.entry.response,
    state.entry.testResults,
    state.hasUnsavedChanges,
    state.introspectionData,
    state.isFetchingIntrospectionData,
    state.hasIntrospectionFailed,
    state.updateEntryRequest,
    state.updateEntry,
    state.getEntry,
    state.updateEntryResponse,
    state.setHasUnsavedChanges,
    state.updateEntryTestResults,
    state.updateEntryScripts,
    state.entry.scripts,
  ]);

  const { apiClientRecordsRepository } = useApiClientRepository();
  const { onSaveRecord } = useNewApiClientContext();
  const { sheetPlacement, toggleSheetPlacement } = useBottomSheetContext();

  const location = useLocation();
  const dispatch = useDispatch();
  const user = useSelector(getUserAuthDetails);

  const { getIsActive, setUnsaved, setTitle, setIcon, getIsNew, setIsNew } = useGenericState();
  const { record } = useApiRecordState(recordId) as { record: RQAPI.GraphQLApiRecord };

  const enableHotkey = getIsActive();

  const [, setDeepLinkState] = useDeepLinkState({ tab: RequestTab.QUERY_PARAMS });

  const [isGeneratingTests, setIsGeneratingTests] = useState(false);
  const [scriptEditorVersion, setScriptEditorVersion] = useState(0);

  const [focusPostResponseScriptEditor, setFocusPostResponseScriptEditor] = useState(false);

  const [isSaving, setIsSaving] = useState(false);
  const [isSending, setIsSending] = useState(false);

  const [error, setError] = useState<RQAPI.ExecutionError | undefined>(undefined);
  const [warning, setWarning] = useState<RQAPI.ExecutionWarning | undefined>(undefined);
  const [isRequestCancelled, setIsRequestCancelled] = useState(false);
  const [isRequestFailed, setIsRequestFailed] = useState(false);
  const [isSchemaBuilderOpen, setIsSchemaBuilderOpen] = useState(true);

  const originalRecord = useRef(getEntry());
  const graphQLRequestExecutor = useGraphQLRequestExecutor(record.collectionId);

  const isHistoryView = location.pathname.includes(PATHS.API_CLIENT.HISTORY.RELATIVE);

  const scopedVariables = useScopedVariables(recordId);

  const handleGenerateTests = useCallback(async () => {
    const entry = getEntry();
    if (!entry?.response || isGeneratingTests) {
      return;
    }

    setIsGeneratingTests(true);
    trackTestGenerationStarted({
      src: "test_tab_response_panel",
    });
    const method = "POST";
    const status = entry.response.status;

    let hasJsonObjectBody = false;
    try {
      const contentType = getContentTypeFromResponseHeaders(entry.response.headers);
      const isJson = /application\/json/i.test(contentType);
      if (isJson) {
        const parsed = JSON.parse(entry.response.body || "null");
        hasJsonObjectBody = parsed && typeof parsed === "object" && !Array.isArray(parsed);
      }
    } catch (_) {
      hasJsonObjectBody = false;
    }

    try {
      const existingScript = entry.scripts?.postResponse || "";
      if (hasTests(existingScript)) {
        setIsGeneratingTests(false);
        return;
      }
      const block = buildAutogeneratedTestsBlock({
        method,
        status,
        hasJsonObjectBody,
      });

      setFocusPostResponseScriptEditor(true);
      const newScript = injectAutogeneratedTests(existingScript, block);
      updateEntryScripts({
        ...(entry.scripts || { preRequest: "", postResponse: "" }),
        postResponse: newScript,
      });

      setScriptEditorVersion((prev) => prev + 1);
      setDeepLinkState({ tab: RequestTab.SCRIPTS });
      trackTestGenerationCompleted({
        src: "test_tab_response_panel",
      });
    } catch (e) {
      toast.error("Something went wrong while generating tests");
      trackTestGenerationFailed({
        src: "test_tab_response_panel",
      });
    } finally {
      setIsGeneratingTests(false);
    }
  }, [getEntry, isGeneratingTests, updateEntryScripts, setDeepLinkState]);

  const canGenerateTests = useMemo(() => {
    const responseExists = Boolean(response);
    if (!responseExists) return false;
    const postResponseScript = scripts?.postResponse || "";
    return !hasTests(postResponseScript);
  }, [response, scripts?.postResponse]);

  const handleUrlChange = useCallback(
    (value: string) => {
      updateEntryRequest({
        url: value,
      });
    },
    [updateEntryRequest]
  );

  const { introspectAndSaveSchema } = useGraphQLIntrospection();
  const [purgeAndAddHeaders] = useAutogenerateStore((state) => [state.purgeAndAddHeaders]);

  const debouncedIntrospection = useDebounce(introspectAndSaveSchema, 500);

  useEffect(() => {
    if (url && isNull(introspectionData)) {
      debouncedIntrospection();
    }
  }, [url, debouncedIntrospection, introspectionData]);

  const handleUrlInputEnterPressed = useCallback((evt: KeyboardEvent) => {
    (evt.target as HTMLInputElement).blur();
  }, []);

  useEffect(() => {
    purgeAndAddHeaders(AutogeneratedFieldsNamespace.CONTENT_TYPE, {
      "Content-Type": createHeaderEntry("Content-Type", RequestContentType.JSON),
    });
  }, [purgeAndAddHeaders]);

  const handleSave = useCallback(async () => {
    const entry = getEntry();
    const apiRecord = createApiRecord(entry, record);
    const operationNames = extractOperationNames(apiRecord.data.request.operation);

    const hasDefaultOrEmptyName = !apiRecord.name || apiRecord.name === DEFAULT_REQUEST_NAME;
    const hasSingleOperation = operationNames.length === 1;

    const recordName =
      hasDefaultOrEmptyName && hasSingleOperation ? operationNames[0] : apiRecord.name || DEFAULT_REQUEST_NAME;

    const recordToSave: Partial<RQAPI.ApiRecord> = {
      ...apiRecord,
      name: recordName,
      type: RQAPI.RecordType.API,
      data: {
        ...apiRecord.data,
      },
    };

    delete apiRecord.data.request.operationName;

    if (isCreateMode) {
      const requestId = apiClientRecordsRepository.generateApiRecordId();
      recordToSave.id = requestId;
    }

    if (apiRecord?.id) {
      recordToSave.id = apiRecord?.id;
    }
    setIsSaving(true);
    const result = isCreateMode
      ? await apiClientRecordsRepository.createRecordWithId(recordToSave, recordToSave.id)
      : await apiClientRecordsRepository.updateRecord(recordToSave, recordToSave.id);

    if (result.success && result.data.type === RQAPI.RecordType.API) {
      onSaveRecord({ ...(apiRecord ?? {}), ...result.data, data: { ...result.data.data, ...recordToSave.data } });
      onSaveCallback(result.data as RQAPI.GraphQLApiRecord);
      setHasUnsavedChanges(false);
      trackRequestSaved({
        src: "api_client_view",
        has_scripts: Boolean(result.data.data.scripts?.preRequest),
        auth_type: result.data.data.auth?.currentAuthType,
        type: RQAPI.ApiEntryType.GRAPHQL,
      });
      toast.success("Request saved!");
    } else {
      toast.error("Something went wrong while saving the request");
    }
    setIsSaving(false);
  }, [getEntry, record, isCreateMode, apiClientRecordsRepository, onSaveRecord, onSaveCallback, setHasUnsavedChanges]);

  const handleRecordNameUpdate = useCallback(
    async (newName: string) => {
      if (!newName || newName === record.name) {
        return;
      }
      const entry = getEntry();
      const apiRecord = createApiRecord(entry, record);

      const isValidHeader = apiRecord.data.request?.headers?.every((header) => {
        return !header.isEnabled || !INVALID_KEY_CHARACTERS.test(header.key);
      });

      const isValidAuthKey =
        apiRecord.data.auth?.currentAuthType !== Authorization.Type.API_KEY ||
        !apiRecord.data.auth?.authConfigStore?.API_KEY?.key ||
        !INVALID_KEY_CHARACTERS.test(apiRecord.data.auth?.authConfigStore?.API_KEY?.key);

      if (!isValidHeader || !isValidAuthKey) {
        notification.error({
          message: `Could not save request.`,
          description: "key contains invalid characters.",
          placement: "bottomRight",
        });
        return;
      }

      const recordToUpdate: Partial<RQAPI.GraphQLApiRecord> = {
        type: RQAPI.RecordType.API,
        data: { ...apiRecord.data },
      };

      if (apiRecord?.id) {
        recordToUpdate.id = apiRecord?.id;
        recordToUpdate.name = newName;
      }

      if (isCreateMode) {
        recordToUpdate.name = newName;
      }

      const result = isCreateMode
        ? await apiClientRecordsRepository.createRecord(recordToUpdate)
        : await apiClientRecordsRepository.updateRecord(recordToUpdate, recordToUpdate.id);

      if (result.success && result.data.type === RQAPI.RecordType.API) {
        setTitle(newName);
        const savedRecord: RQAPI.GraphQLApiRecord = {
          ...(apiRecord ?? {}),
          ...result.data,
          data: { ...result.data.data, ...record.data },
        };
        onSaveRecord(savedRecord);
        trackRequestRenamed("breadcrumb");
        onSaveCallback(savedRecord);
        setTitle(newName);

        toast.success("Request name updated!");
      } else {
        notification.error({
          message: `Could not rename Request.`,
          description: result?.message,
          placement: "bottomRight",
        });
      }
    },
    [apiClientRecordsRepository, record, getEntry, isCreateMode, onSaveCallback, onSaveRecord, setTitle]
  );

  const handleRevertChanges = () => {
    updateEntry(originalRecord.current);
  };

  const resetState = useCallback(() => {
    setError(undefined);
    setWarning(undefined);
    setIsRequestCancelled(false);
    setIsRequestFailed(false);
    updateEntryResponse(null);
  }, [updateEntryResponse]);

  const handleSend = useCallback(
    async (operationName?: string) => {
      if (!isExtensionInstalled() && !isDesktopMode()) {
        /* SHOW INSTALL EXTENSION MODAL */
        const modalProps = {
          heading: "Install browser Extension to use the API Client",
          subHeading:
            "A minimalistic API Client for front-end developers to test their APIs and fast-track their web development lifecycle. Add custom Headers and Query Params to test your APIs.",
          eventPage: "api_client",
        };
        dispatch(globalActions.toggleActiveModal({ modalName: "extensionModal", newProps: modalProps }));
        trackInstallExtensionDialogShown({ src: "api_client" });
        return;
      }

      const entry = getEntry();
      const apiRecord = createApiRecord(entry, record);
      try {
        if (!apiRecord) {
          throw new Error("Record not found");
        }

        resetState();
        setIsSending(true);

        if (operationName) {
          apiRecord.data.request.operationName = operationName;
        } else {
          delete apiRecord.data.request.operationName;
        }

        const apiClientExecutionResult = await graphQLRequestExecutor.executeGraphQLRequest(
          {
            entry: apiRecord.data,
            recordId,
          },
          {
            iteration: 0,
            iterationCount: 1,
          }
        );

        const entryWithResponse = apiClientExecutionResult.executedEntry as RQAPI.GraphQLApiEntry;
        updateEntryResponse(entryWithResponse.response);
        updateEntryTestResults(entryWithResponse.testResults ?? []);
        notifyApiRequestFinished(entryWithResponse);
        trackAPIRequestSent({
          has_scripts: Boolean(apiRecord.data?.scripts?.preRequest),
          auth_type: apiRecord.data?.auth?.currentAuthType,
          request_type: getRequestTypeForAnalyticEvent(apiRecord?.isExample, apiRecord.data?.request?.url),
          type: RQAPI.ApiEntryType.GRAPHQL,
        });

        if (apiClientExecutionResult.status === RQAPI.ExecutionStatus.SUCCESS) {
          if (apiClientExecutionResult.warning) {
            setWarning(apiClientExecutionResult.warning);
          }
        } else if (apiClientExecutionResult.status === RQAPI.ExecutionStatus.ERROR) {
          setError(apiClientExecutionResult.error);
          setIsRequestFailed(true);
        }
      } catch (error) {
        setIsRequestFailed(true);
        setError(error as RQAPI.ExecutionError);
      } finally {
        setIsSending(false);
      }
    },
    [
      getEntry,
      record,
      resetState,
      graphQLRequestExecutor,
      recordId,
      updateEntryResponse,
      updateEntryTestResults,
      notifyApiRequestFinished,
      dispatch,
    ]
  );

  const handleTestResultRefresh = useCallback(async () => {
    try {
      const entry = getEntry();
      const apiRecord = createApiRecord(entry, record);
      const { preparedEntry } = graphQLRequestExecutor.prepareGraphQLRequest(recordId, apiRecord?.data);

      const result = await graphQLRequestExecutor.rerun(recordId, preparedEntry);
      if (result.status === RQAPI.ExecutionStatus.SUCCESS) {
        updateEntryTestResults(result.artifacts.testResults);
      } else {
        setError(result.error);
      }
    } catch (error) {
      toast.error("Something went wrong while refreshing test results");
    }
  }, [getEntry, record, graphQLRequestExecutor, recordId, updateEntryTestResults]);

  const handleCancelRequest = useCallback(() => {
    graphQLRequestExecutor.abort();
    setIsRequestCancelled(true);
  }, [graphQLRequestExecutor]);

  useEffect(() => {
    setIcon(<GrGraphQl />);
  }, [setIcon]);

  useEffect(() => {
    if (isHistoryView) {
      setTitle("History");
    } else {
      setTitle(record.name || "Untitled request");
    }
  }, [setTitle, isHistoryView, record.name]);

  useEffect(() => {
    setUnsaved(hasUnsavedChanges);
  }, [hasUnsavedChanges, setUnsaved]);

  const isDefaultPlacementRef = useRef(false);

  useLayoutEffect(() => {
    if (isDefaultPlacementRef.current) {
      return;
    }

    isDefaultPlacementRef.current = true;
    const bottomSheetPlacement = window.innerWidth <= 1280 ? BottomSheetPlacement.BOTTOM : BottomSheetPlacement.RIGHT;
    toggleSheetPlacement(bottomSheetPlacement);
  }, [toggleSheetPlacement]);

  return (
    <div className="api-client-view gql-client-view">
      <div className="api-client-header-container">
        <RoleBasedComponent
          permission="create"
          resource="api_client_request"
          fallback={
            <Conditional condition={user.loggedIn && !openInModal && hasUnsavedChanges}>
              <RevertViewModeChangesAlert
                title="As a viewer, You can modify and test APIs, but cannot save updates."
                callback={handleRevertChanges}
              />
            </Conditional>
          }
        />
        <div className="api-client-header-container__header">
          <div className="api-client-breadcrumb-container">
            <ApiClientBreadCrumb
              id={record.id}
              placeholder="Untitled request"
              openInModal={openInModal}
              name={record.name ?? "Untitled request"}
              autoFocus={getIsNew()}
              onBlur={(newName) => {
                setIsNew(false);
                handleRecordNameUpdate(newName);
              }}
              breadCrumbType={BreadcrumbType.API_REQUEST}
            />

            <ClientCodeButton
              requestPreparer={() => {
                return graphQLRequestExecutor.prepareGraphQLRequest(recordId, getEntry()).preparedEntry.request;
              }}
            />
          </div>

          <div className="api-client-header__url">
            <Space.Compact className="api-client-url-container">
              <GraphQLClientUrl
                url={url}
                currentEnvironmentVariables={scopedVariables}
                onEnterPress={handleUrlInputEnterPressed}
                onUrlChange={handleUrlChange}
                fetchingIntrospectionData={isFetchingIntrospectionData}
                isIntrospectionDataFetchingFailed={hasIntrospectionFailed}
              />
            </Space.Compact>
            <SendQueryButton disabled={!url} loading={isSending} onSendClick={handleSend} />

            <Conditional condition={!openInModal}>
              <RBACButton
                disabled={!hasUnsavedChanges}
                permission="create"
                resource="api_client_request"
                showHotKeyText
                hotKey={KEYBOARD_SHORTCUTS.API_CLIENT.SAVE_REQUEST.hotKey}
                onClick={handleSave}
                loading={isSaving}
                tooltipTitle="Saving is not allowed in view-only mode. You can update and view changes but cannot save them."
                enableHotKey={enableHotkey}
              >
                Save
              </RBACButton>
            </Conditional>
          </div>
        </div>
      </div>
      <BottomSheetLayout
        layout={SheetLayout.SPLIT}
        bottomSheet={
          <ApiClientBottomSheet
            key={recordId}
            response={response}
            testResults={testResults}
            onGenerateTests={handleGenerateTests}
            isGeneratingTests={isGeneratingTests}
            canGenerateTests={canGenerateTests}
            isLoading={isSending}
            isFailed={isRequestFailed}
            isRequestCancelled={isRequestCancelled}
            onCancelRequest={handleCancelRequest}
            handleTestResultRefresh={handleTestResultRefresh}
            error={error}
            onDismissError={resetState}
            warning={warning}
            executeRequest={handleSend}
          />
        }
        minSize={sheetPlacement === BottomSheetPlacement.BOTTOM ? 25 : 350}
        initialSizes={sheetPlacement === BottomSheetPlacement.BOTTOM ? [60, 40] : [60, 40]}
      >
        <div className="api-client-body">
          <GraphQLRequestTabs
            requestId={recordId}
            collectionId={record.collectionId}
            isSchemaBuilderOpen={isSchemaBuilderOpen}
            setIsSchemaBuilderOpen={setIsSchemaBuilderOpen}
            focusPostResponseScriptEditor={focusPostResponseScriptEditor}
            scriptEditorVersion={scriptEditorVersion}
          />
        </div>
      </BottomSheetLayout>
    </div>
  );
};

const WithGraphQLRecordProvider = (Component: React.ComponentType<any>) => {
  return (props: any) => {
    return (
      <ErrorBoundary boundaryId="graphql-client-view-error-boundary">
        <GraphQLRecordProvider entry={props.apiEntryDetails.data} recordId={props.apiEntryDetails.id}>
          <Component {...props} />
        </GraphQLRecordProvider>
      </ErrorBoundary>
    );
  };
};

export default WithGraphQLRecordProvider(GraphQLClientView);
